/*
 * Copyright (c) 2009 John Pritchard, WTKX Project Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wtkx.ui;

/**
 * Text position includes caret and selection as offset and length.
 * 
 * This class is responsible for caret and selection and text editing.
 * It effects caret and selection changes to the {@link wtkx.Text}
 * component's string buffer, and receives events for all caret
 * positioning input.
 * 
 * <h3>Operation</h3>
 * 
 * The position offset serves two related but not quite identical
 * roles.  It locates the caret in the glyph vector, and editing
 * operations in the string buffer.  It is determined by mouse and
 * keyboard by positioning and reckoning.
 * 
 * The caret must reach both beginning and end of a line.  The
 * beginning of line position is the leading edge of the respective
 * character offset.  The end of line position is the trailing edge of
 * the respective character offset.
 * 
 * 
 * @see wtkx.Text
 * @see Caret
 * @see Selection
 * @author jdp
 */
public final class Position 
    extends Object
    implements GV.Aware,
               Cloneable
{
    /**
     * Interface to {@link wtkx.Text} or others like it.
     */
    public interface TextComponent {

        public StringBuilder textBuffer();

        public GV textGlyphs();

        public Caret textCaret();
    }


    private final static boolean Leading = false;

    private final static boolean Trailing = true;


    private TextComponent component;

    private StringBuilder buffer;
    /**
     * Glyph pointer for into Buffer and GV.
     */
    protected int offset;
    /**
     * Selection count
     */
    protected int count;
    /**
     * Caret position relative to offset.
     */
    protected boolean trailing;
    /**
     * Status of 'offset', 'trailing' and 'count'.
     */
    private boolean changed;


    public Position(TextComponent component){
        super();
        if (null != component){
            this.component = component;
            this.buffer = component.textBuffer();
            if (null == this.buffer)
                throw new IllegalArgumentException();
            else {
                this.init();
            }
        }
        else
            throw new IllegalArgumentException();
    }


    public void destroy(){
        this.component = null;
    }
    public void init(){
        synchronized(this){
            this.offset = -1;
            this.count = 0;
            this.trailing = true;
            this.changed = true;
        }
    }
    private boolean offset(int ofs){
        ofs = this.getPosition(ofs);
        if (ofs != this.offset){
            synchronized(this){
                this.offset = ofs;
                return (this.changed = true);
            }
        }
        return false;
    }
    private boolean count(int len){
        if (len != this.count){
            synchronized(this){
                this.count = len;
                return (this.changed = true);
            }
        }
        else
            return false;
    }
    private boolean trailing(boolean b){
        if (b != this.trailing){
            synchronized(this){
                this.trailing = b;
                return (this.changed = true);
            }
        }
        else
            return false;
    }
    public boolean home(int ofs){
        if (0 == ofs){
            boolean re = this.offset(-1);
            re |= this.trailing(true);
            return (this.count(0) || re);
        }
        else if (0 < ofs){
            {
                int skip = 0;
                while (ofs < this.buffer.length() && '\n' == this.buffer.charAt(ofs)){
                    skip += 1;
                    ofs += 1;
                }
                if (0 < skip) ofs -= 1;
            }
            boolean re = this.offset(ofs);
            re |= this.trailing(false);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean left(){
        if (-1 < this.offset){
            boolean re = this.offset(this.offset-1);
            re |= this.trailing(false);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean right(){
        boolean re = this.offset(this.offset+1);
        re |= this.trailing(true);
        re |= this.count(0);
        return re;
    }
    public boolean up(int ofs){
        boolean re = this.offset(ofs);
        re |= this.trailing(true);
        re |= this.count(0);
        return re;
    }
    public boolean down(int ofs){
        boolean re = this.offset(ofs);
        re |= this.trailing(true);
        re |= this.count(0);
        return re;
    }
    public boolean mouse(int ofs){
        boolean re = this.offset(ofs);
        re |= this.trailing(true);
        re |= this.count(0);
        return re;
    }
    public boolean key(int ofs){
        boolean re = this.offset(ofs);
        re |= this.trailing(true);
        re |= this.count(0);
        return re;
    }
    public boolean insert(int ofs, int len){
        if (-1 < ofs && 0 < len){
            boolean re = this.offset(ofs+len-1);
            re |= this.trailing(true);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean delete(int start, int end){
        if (-1 < start){
            boolean re = this.offset(start-1);
            re |= this.trailing(true);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean delete(int start, int end, Direction direction){
        if (-1 < start){
            boolean re = this.offset(start-1);
            re |= this.trailing(true);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean end(int end){
        if (0 < end){
            boolean re = this.offset(end-1);
            re |= this.trailing(true);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean append(int end){
        if (0 < end){
            boolean re = this.offset(end-1);
            re |= this.trailing(true);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }
    public boolean replace(int start, int end, int with){
        if (-1 < start && 0 < with){
            boolean re = this.offset(start+with-1);
            re |= this.trailing(true);
            re |= this.count(0);
            return re;
        }
        else
            return false;
    }

    public boolean select(int ofs){
        if (-1 < ofs){
            int start = this.offset;
            int count;
            if (start > ofs)
                count = (start - ofs);
            else
                count = (ofs - start);

            return this.count(count);
        }
        else
            return false;
    }
    public boolean select(int ofs, int len){
        if (-1 < ofs){
            boolean re = this.offset(Math.max(0,ofs));
            re |= this.trailing(false);
            return (this.count(len) || re);
        }
        else
            return false;
    }
    public boolean selectLeft(){
        if (-1 < this.offset){
            int ofs = Math.max(0,this.getOffsetLeading()-1);
            boolean re = this.offset(ofs);
            re |= this.trailing(false);
            re |= this.count(this.count+1);
            return re;
        }
        else
            return false;
    }
    public boolean selectRight(){
        int ofs = Math.max(0,this.offset);
        boolean re = this.offset(ofs);
        re |= this.trailing(false);
        re |= this.count(this.count+1);
        return re;
    }
    public boolean selectUp(int ofs){
        if (-1 < ofs){
            int start = Math.max(0,this.getOffsetLeading());
            int s = Math.min(start,ofs);
            if (s < start){
                boolean re = this.offset(s);
                re |= this.trailing(false);
                re |= this.count(start-s);
                return re;
            }
        }
        return false;
    }
    public boolean selectDown(int ofs){
        if (-1 < ofs){
            int start = Math.max(0,this.offset);
            if ((start+this.count) < ofs){
                boolean re = this.offset(start);
                re |= this.trailing(false);
                re |= this.count(ofs-start);
                return re;
            }
        }
        return false;
    }

    public Position clone(){
        try {
            return (Position)super.clone();
        }
        catch (CloneNotSupportedException exc){
            throw new InternalError(exc.getClass().getName());
        }
    }
    public boolean isEmpty(){
        return (0 == this.count);
    }
    public boolean isNotEmpty(){
        return (0 != this.count);
    }
    public boolean isChanged(){
        return this.changed;
    }
    public boolean unsetChanged(){
        boolean re = this.changed;
        this.changed = false;
        return re;
    }
    public boolean isInsert(){
        int ofs = this.getOffsetLeading();
        return (ofs < this.buffer.length());
    }
    public boolean isAppend(){
        int ofs = this.getOffsetLeading();
        return (ofs >= this.buffer.length());
    }
    public boolean is(Caret c){
        if (null == c)
            return false;
        else
            return (c.position == this.offset)&&(c.trailing == this.trailing);
    }
    public boolean isNot(Caret c){
        if (null == c)
            return true;
        else
            return (c.position != this.offset)||(c.trailing != this.trailing);
    }
    public boolean is(Selection sel){
        if (null == sel)
            return false;
        else
            return (sel.start == this.getOffsetLeading())&&(sel.end == this.getEnd());
    }
    public boolean isNot(Selection sel){
        if (null == sel)
            return true;
        else
            return (sel.start != this.getOffsetLeading())||(sel.end != this.getEnd());
    }
    public int getLength(){
        return this.count;
    }
    public int getEnd(){
        return (this.offset+this.count);
    }
    public int getLast(){
        return (this.offset+this.count-1);
    }
    public int getTerminal(){
        int end = this.buffer.length();
        if (0 == end)
            return 0;
        else
            return (end-1);
    }
    public int getPosition(){
        return Math.min(this.offset,this.getTerminal());
    }
    public int getPosition(int ofs){
        return Math.min(ofs,this.getTerminal());
    }
    public int getOffset(){
        return this.offset;
    }
    public int getOffsetLeading(){
        if (this.trailing)
            return (this.offset+1);
        else
            return (this.offset);
    }

    public boolean keyTyped(char ch, GV g, TextListener listener){

        String string = String.valueOf(ch);

        if (this.isNotEmpty())
            return this.replace(string,g,listener);
        else {
            StringBuilder buffer = this.buffer;

            int ofs = this.getOffsetLeading();

            if (ofs < buffer.length()){

                buffer.insert(ofs,ch);

                this.key(ofs);

                if (null != listener)
                    listener.charactersInserted(ofs,string);
            }
            else {

                buffer.append(ch);

                this.append(buffer.length());

                if (null != listener)
                    listener.charactersAppended(string);
            }
            return true;
        }
    }
    /*
     * These buffer operators are here so that this class can take
     * responsibility for the offset states and their arithmetic.
     */
    public boolean append(String string, GV g, TextListener listener){
        StringBuilder buffer = this.buffer;
        if (null != string){
            int strlen = string.length();
            if (0 < strlen){

                buffer.append(string);

                this.append(buffer.length());

                if (null != listener)
                    listener.charactersAppended(string);

                return true;
            }
        }
        return false;
    }
    public boolean insert(String string, GV g, TextListener listener){
        StringBuilder buffer = this.buffer;
        if (null != string){
            int strlen = string.length();
            if (0 < strlen){

                int ofs = this.getOffsetLeading();

                buffer.insert(ofs,string);

                this.insert(ofs,strlen);

                if (null != listener)
                    listener.charactersInserted(ofs,string);

                return true;
            }
        }
        return false;
    }
    public String delete(Direction direction, GV g, TextListener listener){
        StringBuilder buffer = this.buffer;

        int count = this.count;
        int end;
        if (0 != count){
            int start = this.offset;
            end = (start+count);
            if (end <= buffer.length()){

                String cut = buffer.substring(start,end);
                if (null != cut){

                    buffer.delete(start,end);

                    this.delete(start,end,direction);

                    if (null != listener)
                        listener.charactersRemoved(start,end);
                }
                return cut;
            }
            else
                return null;
        }
        else if (Direction.Backward == direction){
            int start = this.getOffsetLeading();
            int pre = (start-1);
            if (-1 < pre && start <= buffer.length()){

                String cut = buffer.substring(pre,start);
                if (null != cut){

                    buffer.delete(pre,start);

                    this.delete(pre,start,direction);

                    if (null != listener)
                        listener.charactersRemoved(pre,start);
                }
                return cut;
            }
            else
                return null;
        }
        else {
            int start = this.getOffsetLeading();
            end = (start+1);
            if (end <= buffer.length()){

                String cut = buffer.substring(start,end);
                if (null != cut){

                    buffer.delete(start,end);

                    this.delete(start,end,direction);

                    if (null != listener)
                        listener.charactersRemoved(start,end);
                }
                return cut;
            }
            else
                return null;
        }
    }
    public boolean replace(String string, GV g, TextListener listener){
        int count = this.count;
        if (0 != count){
            int start = this.offset;
            if (null != string){
                int strlen = string.length();
                if (0 < strlen){
                    int end = (start+count);

                    this.buffer.replace(start,end,string);

                    this.replace(start,end,strlen);

                    if (null != listener)
                        listener.charactersReplaced(start,end,string);

                    return true;
                }
            }
            return false;
        }
        else {
            return this.insert(string,g,listener);
        }
    }
}
